// Finally, a UNIX command-line utility for converting VZ200/VZ300/Laser
// binaries into audio for uploading through the cassette interface.
// Based on what I gleaned from a disassembly of the VZ300 ROM.
// Thanks to Alan VK2EBA for the free '300!
// --
// Usage vz2wav [-v :use ".VZ" file header]
//              [-s startaddress :decimal,, default 31465]
//              [-t filetype :decimal, default 240,"T"]
//              program.bin
//              > program.wav  (or "| play -")
//
//    $ vz2wav -v CHESS.VZ | play -
//    $ vz2wav -s 32768 -t 241 code.bin | play -
//
// Noting, with my computer at least, the audio volume needs to be at
// maximum levels for the VZ to hear it.
// --
// Copyright 2021 Chris Baird <chris.j.baird@gmail.com> 2021-Nov-4.
// Licenced to you according to the GNU Public Licence Version 3.
// --
// Don't go porting this to Windows or Smartphones and charging people money
// without giving them all the relevant source code, y'hear. GPL says so,
// that's why. Yes, I know they're helpless and and ripe for ripping-off,
// that 99% of them have no idea what source code even is, and in the 21st
// Century we're all supposed to be super-sweaty about getting rich from the
// Internet, but we're 8bitter programmers dammit.

#define AUDIORATE (48000) // 44100, 32000, 22050, 11025, and even 8000 works..
//#define Z80CPUCLOCK (3580000) // VZ200 - untested
#define Z80CPUCLOCK (3546894) // VZ300=(17734470/5)
#define DURBIT1 (988)	      // Clock/'1' half-wavelength 13*76=988 cycles
#define DURBIT0 (1989)	      // '0' bit half-wavelength 13*153=1989 cycles

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <err.h>

#pragma pack(push, 1)		   // align at byte-boundary, no padding
typedef struct wav_header
{
  char riff_header[4];	           // Contains "RIFF"
  unsigned int wav_size;           // Size of the wav portion of the file
  char wave_header[4];	           // Contains "WAVE"
  char fmt_header[4];              // Contains "fmt "
  unsigned int fmt_chunk_size;     // Should be 16 for PCM
  unsigned short audio_format;     // Should be 1 for PCM. 3 for IEEE Float
  unsigned short num_channels;
  unsigned int sample_rate;
  unsigned int byte_rate;      // sample_rate * num_channels * Bytes Per Sample
  unsigned short sample_alignment; // num_channels * Bytes Per Sample
  unsigned short bit_depth;	   // Number of bits per sample
  char data_header[4];		   // Contains "data"
  unsigned int data_bytes;	   // samples * num_channels * sample byte size
} wav_header;
#pragma pack(pop)

typedef struct vzheader
{
  unsigned char vzmagic[4];
  unsigned char filename[17];
  unsigned char filetype;
  unsigned char start_l;
  unsigned char start_h;
} vzstruct;

unsigned char bits[1900];
unsigned int bitslen = 0, checksum = 0, totalsamples = 0;


void wav_header_out (void)
{
  wav_header h;
  h.riff_header[0] = 'R'; h.riff_header[1] = 'I'; h.riff_header[2] = 'F';
  h.riff_header[3] = 'F'; h.wav_size = 4294967295L; h.wave_header[0] = 'W';
  h.wave_header[1] = 'A'; h.wave_header[2] = 'V'; h.wave_header[3] = 'E';
  h.fmt_header[0] = 'f'; h.fmt_header[1] = 'm'; h.fmt_header[2] = 't';
  h.fmt_header[3] = ' '; h.fmt_chunk_size = 16; h.audio_format = 1;
  h.num_channels = 1; h.sample_rate = AUDIORATE; h.byte_rate = AUDIORATE*1*2;
  h.sample_alignment = 2; h.bit_depth = 16; h.data_header[0] = 'd';
  h.data_header[1] = 'a'; h.data_header[2] = 't'; h.data_header[3] = 'a';
  h.data_bytes = 4294967295L;
  fwrite (&h, 1, sizeof(h), stdout);
}


// A Bresenham-ish method to deal with the audio wavelengths being based on
// cycles of a CPU with an odd clock frequency..

void outputwavfragment (void)
{
  static unsigned long T = 0, C = 0;
  static int cyclecount = 0;
  const short v[3] = { -16384, 16384, 0};
  int p = 0;

  while (p < bitslen)
    {
      C += AUDIORATE;
      cyclecount++;
      if (C >= Z80CPUCLOCK)
        {
          C -= Z80CPUCLOCK;
          T += cyclecount;
          cyclecount = 0;
          fwrite (&v[bits[p]], 1, 2, stdout);
          totalsamples++;
          if (T >= DURBIT1)
            {
              T -= DURBIT1;
              p++;
            }
        }
    }
}


void modulate_byte (unsigned char c)
{
  checksum += c;

  for (unsigned char b = 128; b; b >>= 1)
    {
      bits[bitslen++] = 1;
      bits[bitslen++] = 0;
      bits[bitslen++] = 1;
      if ((c & b) == 0)
        {
          bits[bitslen++] = 1;
          bits[bitslen++] = 0;
        }
      else
        {
          bits[bitslen++] = 0;
          bits[bitslen++] = 1;
        }
      bits[bitslen++] = 0;
    }
  outputwavfragment();
  bitslen = 0;
}


int main (int argc, char *argv[])
{
  FILE *fp;
  vzstruct vz;
  int i, namelen, parsevz = 0;
  unsigned int startaddr = 0x7AE9, endaddr = 0, filetype = 0xF0;

  while ((i = getopt(argc, argv, "s:t:e:v")) != -1)
    switch (i)
      {
      case 's': startaddr = atoi (optarg); break;
      case 't': filetype = atoi (optarg); break;
      case 'v': parsevz = 1; break;
      }
  argc -= optind;
  argv += optind;

  char *name = argv[0];
  if (name == NULL) err (EXIT_FAILURE, "error: luser at keyboard\n");

  fp = fopen (name, "r");
  if (fp == NULL) err (EXIT_FAILURE, "could not open \"%s\"\n", name);

  fseek (fp, 0, SEEK_END);
  long filesize = ftell (fp);
  fseek (fp, 0, SEEK_SET);

  if (parsevz)
    {
      fread (&vz, sizeof(vz), 1, fp);
      startaddr = vz.start_l | (vz.start_h << 8);
      name = (char*)vz.filename;
      filetype = vz.filetype;
      filesize -= sizeof(vz);
    }

  endaddr = startaddr + filesize;
  namelen = strlen (name);

  //

  wav_header_out();

  for (i = 1800; i; i--) bits[bitslen++] = 2; // ~500ms pause

  for (i = 63; i; i--) modulate_byte (0x80); // ROM does this 255 times
  for (i =  5; i; i--) modulate_byte (0xFE);

  modulate_byte (filetype); // 0=T 1=B 2=D 3=W 4=A 5=I 241=autoexec 242=Data?

  if (namelen > 18) namelen = 18; // ROM says 18..
  for (i = 0; i < namelen; i++)
    {
      char c = name[i] & 127;
      if ((c >= 'a') && (c <= 'z')) c -= 32;
      if (c >= 32) modulate_byte (c);
    }
  modulate_byte (0x00);

  // 4mS pause; probably done so that CLOAD's displaying the filename
  // doesn't drop bits
  for (i = 15; i; i--) bits[bitslen++] = 2;

  checksum = 0;
  modulate_byte (startaddr & 255); modulate_byte (startaddr >> 8);
  modulate_byte (endaddr & 255); modulate_byte (endaddr >> 8);

  for (i = 0; i < filesize; i++) modulate_byte (fgetc (fp));

  unsigned int c = checksum & 65535;
  modulate_byte (c & 255); modulate_byte (c >> 8);

  // done by the ROM, but not really required when real tapes aren't involved
  for (i = 20; i; i--) modulate_byte (0x00);

  fprintf (stderr,
	   "start=%04X end=%04X filetype=%X cksum=%04X duration=%ds\n",
	   startaddr, endaddr, filetype, c, totalsamples/AUDIORATE);
  fclose (fp);
}
